import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.JOptionPane;

public class ChatClient implements ChatInterface {

    private ChatGUI gui;
    private InetAddress address = null;
    private Socket connection = null;
    private Thread t1 = null;
    boolean knownDisconnect = false;
    private String srvCapability = "";
    private final ConcurrentHashMap<String, XcherServer> xcherSocket = new ConcurrentHashMap<String, XcherServer>();

    public static void main(String args[]) {
	ChatGUI.create("LeetSpeak", new ChatClient());
    }

    @Override
    public void setGUI(ChatGUI gui) {
	this.gui = gui;
    }

    public ChatGUI getGui() {
	return gui;
    }

    // For Xcher, stores nick -> threadjob mapping.
    public ConcurrentHashMap<String, XcherServer> getXcherSocket() {
	return xcherSocket;
    }

    public void setConnection(Socket connection) {
	this.connection = connection;
    }

    public Socket getConnection() {
	return connection;
    }

    @Override
    public void connectChatServer(String host, String port, String nickname) {
	/*
	 * Connects to the supplied chatserver at host:port with nickname. This
	 * method is called from the GUI when the user clicks the connect
	 * button. It also sends the basic commands such as NAMES upon
	 * connecting so the user wont have to do this manually.
	 */
	try {
	    address = InetAddress.getByName(host);
	    connection = new Socket(address, Integer.parseInt(port));

	    SrvMsgHandler srvMsgHandler = new SrvMsgHandler(this);
	    t1 = new Thread(srvMsgHandler);
	    t1.start();

	    String capabilityMsg = "A001 CAPABILITY\r\n";
	    connection.getOutputStream().write(capabilityMsg.getBytes("UTF-8"));
	    String nickMsg;

	    if (nickname.contains(" ")) {
		nickMsg = "A002 NICK \"" + nickname + "\"\r\n";
	    } else {
		nickMsg = "A002 NICK " + nickname + "\r\n";
	    }

	    connection.getOutputStream().write(nickMsg.getBytes("UTF-8"));

	    String joinMsg = "A006 JOIN\r\n";
	    connection.getOutputStream().write(joinMsg.getBytes("UTF-8"));

	    String namesMsg = "A007 NAMES\r\n";
	    connection.getOutputStream().write(namesMsg.getBytes("UTF-8"));

	    /* gui related connect code */
	    gui.setChatEnabled(true);
	    gui.setChatTitle("Chat: " + host + " : " + port);
	    gui.setSelectedTab(1);

	} catch (IOException e) {
	    JOptionPane.showMessageDialog(gui,
		    "Failed to establish connection to server.", "Error",
		    JOptionPane.ERROR_MESSAGE);
	}

    }

    @Override
    public void disconnectChatServer() {
	/*
	 * disconnects from the chatserver. This method is called from the GUI
	 * when the user clicks the disconnect button or closes the GUI window.
	 * This method also kills any currently running Xcher threads to
	 * properly clean up.
	 */
	knownDisconnect = true;
	// kill all open filesharing sessions.
	for (XcherServer s : xcherSocket.values()) {
	    s.addToQueue("X_END");
	}

	try {
	    // This always runs on exit whether there's a connection or not.
	    // So we must check if a graceful exit is needed.
	    if (connection != null) {
		connection.getOutputStream().write(
			"A009 QUIT\r\n".getBytes("UTF-8"));
		// close thread that handles server messages.
		t1.interrupt();
		connection.close();
		connection = null;
	    }
	} catch (Exception e) {
	    t1.interrupt();
	    try {
		connection.close();
	    } catch (Exception e1) {
	    }
	}

	/* gui related disconnect code */
	gui.setFilesharingEnabled(false);
	gui.setChatEnabled(false);
	gui.setChatTitle("Chat");
	gui.setSelectedTab(0);
	gui.clear();
    }

    @Override
    public ChatClient getClient() {
	return this;
    }

    public void sendXcherCom(String nick, String cmd) {
	/*
	 * This method is used in the communication between the client and its
	 * GUI for XCher related functionality. Different GUI buttons reflect
	 * XCHER specific commands which is sent to this method with the
	 * nickname the command is meant to be sent to. This method then checks
	 * if there's an available connection to the specified nick or if a
	 * connection has to be established. Lastly it adds the command to the
	 * work queue of the thread handling that specific target nick.
	 */
	String ntmp = nick;

	if (xcherSocket.containsKey(nick) && !xcherSocket.get(nick).isSupport()) {
	    // if the target nick has been detected as not supporting XCHER we
	    // ignore this attempt to communicate.
	    return;
	}

	if (!xcherSocket.containsKey(nick)) {
	    XcherServer tjob = new XcherServer(nick, this);
	    xcherSocket.put(nick, tjob);
	    new Thread(tjob).start();

	    // send X_INIT to chatserver for redirection to nick.
	    if (ntmp.contains(" ")) {
		ntmp = "\"" + ntmp + "\"";
	    }

	    String xinitMsg = "X_INIT " + ntmp + " "
		    + connection.getLocalAddress().getHostAddress() + " "
		    + tjob.getServComSocket().getLocalPort() + "\r\n";
	    try {
		connection.getOutputStream().write(xinitMsg.getBytes("UTF-8"));
	    } catch (IOException e) {
		e.printStackTrace();
	    }

	}
	// the message/job to the communication thread for this nick.
	xcherSocket.get(nick).addToQueue(cmd);
    }

    @Override
    public void send(String text) {
	/*
	 * This method handles whatever the user writes in the send input
	 * textfield. If the text string starts with a / it is interpreted as a
	 * command and handled as such. Otherwise its sent straight as a TALK
	 * message to the chatserver.
	 */

	// If the text received starts with / it's interpreted as a command.
	try {
	    if ((text.length() > 0) && (text.charAt(0) == '/')) {
		// if the command is TOPIC or WHOIS then
		// refuse if server does not support it.
		if (text.startsWith("/TOPIC")) {
		    if (!srvCapability.contains(" TOPIC ")) {
			gui.println("TOPIC feature is not supported by this server.");
			return;
		    }
		} else if (text.startsWith("/WHOIS")) {
		    if (!srvCapability.contains(" WHOIS ")) {
			gui.println("WHOIS feature is not supported by this server.");
			return;
		    }

		} else if (text.startsWith("/X_PRIV")) {
		    if (!srvCapability.contains(" XPRIV ")) {
			gui.println("X_PRIV feature is not supported by this server.");
			return;
		    }
		}
		String commandMsg = "A010 " + text.replaceFirst("/", "")
			+ "\r\n";

		connection.getOutputStream()
			.write(commandMsg.getBytes("UTF-8"));

	    } else {
		String TalkMsg = "A010 TALK " + text + "\r\n";
		connection.getOutputStream().write(TalkMsg.getBytes("UTF-8"));
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}

    }

    private class SrvMsgHandler implements Runnable {
	/*
	 * This runnable class is assigned to the Thread that takes care of
	 * response messages from the chatserver. It is essentially the engine
	 * of the chatclient. It reads the input from the server and acts
	 * according to the type of message it received. For example when it
	 * detects an X_INIT message it spawns an XcherClient thread to handle
	 * that filesharing session. This is where messages gets translated into
	 * actions and reactions.
	 */
	BufferedReader msgReader;
	ChatClient clientThread;
	HashMap<String, String> chatNicks = new HashMap<String, String>();

	public SrvMsgHandler(ChatClient chClient) throws IOException {
	    clientThread = chClient;
	    msgReader = new BufferedReader(new InputStreamReader(chClient
		    .getConnection().getInputStream(), "UTF-8"));

	}

	@Override
	public void run() {
	    // matchList holds all the isolated words split from the input
	    // message by regexp.
	    ArrayList<String> matchList = new ArrayList<String>();
	    // regexp pattern isolates full words and those enclosed in "".
	    Pattern regex = Pattern.compile("[^\\s\"']+|\"[^\"]*\"|'[^']*'");
	    while (connection != null) {

		try {

		    String msg = msgReader.readLine();

		    matchList.clear();
		    int limit = 0;

		    Matcher regexMatcher = regex.matcher(msg);
		    while (regexMatcher.find()) {
			matchList.add(regexMatcher.group());
			limit++;

			if (limit == 2) {
			    // We only need to isolate the 2 first strings
			    // for most messages. The rest of the content is
			    // dumped to index 3 and is processed later if
			    // needed.
			    matchList.add(msg.substring(regexMatcher.end())
				    .trim());
			    break;
			}
		    }

		    // Ignore the message if its a A999 tag from sent PING.
		    if (matchList.get(0).equals("A999")) {
			continue;
		    }
		    String nTmp = matchList.get(1);
		    if (nTmp.equals("NAMES") || nTmp.equals("JOIN")) {
			// NAMES, received alias of chat member.
			// JOIN, someone joined chatroom.
			// add to gui namelist.
			String nMsg = matchList.get(2).replace("\"", "");
			if (!chatNicks.containsValue(nMsg)) {
			    chatNicks.put(nMsg, nMsg);
			    Object[] atmp = chatNicks.values().toArray();
			    gui.setClientList(atmp);
			    gui.setXcherUserList(atmp);

			}
		    } else if (nTmp.equals("TOPIC")) {
			gui.println(msg);
			gui.setChatTopic(matchList.get(2));
		    } else if (nTmp.equals("WHOIS")) {
			gui.println(msg);
		    } else if (nTmp.equals("RENAME")) {
			// RENAME, someone changed chat alias.Change in gui
			// namelist.
			gui.println(msg);

			// Requires further string isolation.
			while (regexMatcher.find()) {
			    matchList.add(regexMatcher.group());
			}

			String old_tmp = matchList.get(3).replace("\"", "");
			String new_tmp = matchList.get(4).replace("\"", "");
			chatNicks.remove(old_tmp);
			chatNicks.put(new_tmp, new_tmp);

			Object[] atmp = chatNicks.values().toArray();
			gui.setClientList(atmp);
			gui.setXcherUserList(atmp);

		    } else if (nTmp.equals("LEAVE")) {
			// LEAVE, someone left chatroom, remove from gui
			// namelist.
			chatNicks.remove((matchList.get(2).replace("\"", "")));
			Object[] atmp = chatNicks.values().toArray();
			gui.setClientList(atmp);
			gui.setXcherUserList(atmp);
			gui.println(msg);

		    } else if (nTmp.equals("CAPABILITY")) {
			srvCapability = "";
			// Requires further string isolation.
			while (regexMatcher.find()) {
			    matchList.add(regexMatcher.group());
			}
			// Append every extra feature string to
			// srvCapability.
			for (int i = 4; i < matchList.size(); i++) {
			    String tmp = matchList.get(i);
			    // enable filesharing tabs if the server
			    // supports the same filesharing plugin as this
			    // client.
			    if (tmp.equals("XCHER")) {
				gui.setFilesharingEnabled(true);
			    }
			    srvCapability += " " + tmp + " ";
			}
			gui.println(msg);
		    } else if (matchList.get(0).equals("X_INIT")) {
			while (regexMatcher.find()) {
			    matchList.add(regexMatcher.group());
			}
			// someone requested an XCHER session. Spawn a
			// client thread
			new Thread(new XcherClient(matchList.get(3),
				matchList.get(4), clientThread)).start();
		    } else {
			gui.println("Server: " + msg);
		    }

		} catch (Exception e) {
		    // if we requested the socket to close this message is
		    // irrelevant.
		    if (!knownDisconnect) {
			JOptionPane.showMessageDialog(gui, "Connection lost.",
				"Error", JOptionPane.ERROR_MESSAGE);
		    }
		    // set connect to null to prevent disconnectChatServer()
		    // from attempting a "soft" disconnect.
		    clientThread.setConnection(null);
		    clientThread.getGui().clickM_disconnectBtn();
		    break;
		}
	    }

	}
    }

}